#include "nzbqueue.h"
#include "manager.h"
#include "nntp.h"
#include "global.h"
#include "stats.h"
#include "ydec.h"
#include "logger.h"
#include "remotectrl.h"
#include "filesys.h"

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include <pthread.h>

#if defined(WIN32) || defined(WINCE)
#include <windows.h>
#define stricmp _stricmp
/*
SteelNzb
Copyright (C) 2008  Wessel v. Staal

This program is free software; you can redistribute it and/or
modify it under the terms of the GNU General Public License
as published by the Free Software Foundation; either version 2
of the License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

#define unlink(x) DeleteFileA(x)
#ifndef WINCE
#include <direct.h>
#endif
#else
#include <unistd.h>
#define stricmp strcasecmp
#endif

int manager_connectNntp(void** nntpstate, void* managerstate);
void manager_downloadArticleProgress(void* userdata, int downloadSize);
int manager_downloadArticle(void* state, nzbFile* file, nzbFilePart* article, char* path);
void* manager_downloadThread(void* managerstate);
void manager_logListener(void* userData, char* message);
void manager_deleteFiles(char* basePath, int files);
void manager_remoteControlCommand(void* userData, char* key, char* value);
int manager_combineFiles(void*state, char* basePath, char* targetPath, int files);

typedef struct managerState
{
	void* currentNzbQueue;
	int maxConnectTries;
	int maxArticleTries;
	char username[256];
	char password[256];
	char host[256];
	int port;
	int maxThreads;
	int usesAuthentication;
	void* currentNzbState;
	char downloadPath[1024];
	char nzbPath[1024]; /* path to xsd */
	int enableRemoteControl;
	int enableYEnc;
	int enableFileCombine;
	int isPaused;
	pthread_t* threadPool;
	void* logger;
	void* stats;
	void* remote;
} managerState;

/**
	Processes nzb file. Downloads files and optionally decodes them.
	This function returns once the threads finished processing, so it blocks.
	Not thread safe when using the same state
	@param managerstate Pointer to manager state
	@param nzbfile Path to nzb file
	@returns pointer to manager state
*/
int manager_download(void* managerstate, char* nzbfile)
{
	int i = 0;
	managerState* state = managerstate;
	int articleCount = 0;
	char number[10];
	char xsdPath[1048];
	/* open the nzb file */
	if ( (state->currentNzbState = nzb_openNzb(nzbfile)) == NULL )
	{
		logger_log(state->logger, "StartDownload::Could not open nzb file '%s'", nzbfile, NULL, NULL);
		return 0;
	}

	/* build the queue state */
	if ( (state->currentNzbQueue = nzbqueue_createNzbQueueState(state->currentNzbState)) == NULL )
	{
		logger_log(state->logger, "StartDownload::Unable to initialize queue", NULL, NULL, NULL);
		nzb_closeNzb(state->currentNzbState);
		state->currentNzbState = NULL;
		return 0;
	}

	strcpy(xsdPath, state->nzbPath);
	strcat(xsdPath, "nzb.xsd");

	nzbqueue_setUserData(state->currentNzbQueue, managerstate);


	if ( (articleCount = nzb_getArticleCountAndValidate(state->currentNzbState, xsdPath)) == -1 )
	{
		logger_log(state->logger, "StartDownload::Unable to validate nzb file", NULL, NULL, NULL);
		nzb_closeNzb(state->currentNzbState);
		nzbqueue_freeQueue(state->currentNzbQueue);
		state->currentNzbState = NULL;
		state->currentNzbQueue = NULL;
		return 0;
	}

	/* count all parts and assign the result to the stats */
	stats_setFileCount(state->stats, articleCount);
	stats_setCurrentNzbFile(state->stats, nzbfile);
	stats_setProcessed(state->stats, 0);

	sprintf(number, "%i", articleCount);

	logger_log(state->logger, "StartDownloader::Starting download, %s parts", number, NULL, NULL);
	/* create the worker threads */
	for ( i=0; i<state->maxThreads; i++ )
	{
		sprintf(number, "%i", i+1);

		logger_log(state->logger, "StartDownloader::Spawning thread #%s", number, NULL, NULL);
		if ( pthread_create(&state->threadPool[i], NULL, manager_downloadThread, state) != 0 )
		{
			perror("manager::Download");
			exit(1);
		}
	}

	/* join all threads */
	for ( i=0; i<state->maxThreads; i++ )
	{
		if ( pthread_join(state->threadPool[i], NULL) != 0 )
		{
			perror("manager::Download");
		}
		sprintf(number, "%i", i+1);
		logger_log(state->logger, "StartDownloader::Joined thread #%s", number, NULL, NULL);
	}

	/* after this, release resources related to this download */
	nzbqueue_freeQueue(state->currentNzbQueue);

	nzb_closeNzb(state->currentNzbState);

	state->currentNzbQueue = NULL;
	state->currentNzbState = NULL;

	return 1;
}

/**
	Default listener for the logger
	Prints data to the remote control when enabled
*/
void manager_logListener(void* userData, char* message)
{
	managerState* managerstate = userData;
	
	PRINTERROR(message);

	if ( managerstate->enableRemoteControl && managerstate->remote != NULL )
		remotectrl_sendLogMessage(managerstate->remote, message);

}

/** Pauses queue */
void manager_setPause(void* state, int pause)
{
	managerState* managerstate = state;

	managerstate->isPaused = pause;
	stats_setIsPaused(managerstate->stats, pause);
}

/**
	Creates manager state
	@param serveraddress Dns name or ip of nntp server
	@param port Port of nntp server
	@param remoteControl int indicating remote control is enabled
	@param remoteControlPort The port for remote control
	@returns pointer to manager state
*/
void* manager_createState(char* serveraddress, int port, int remoteControl, int remoteControlPort)
{
	managerState* state = NULL;
	
	if ( (state = malloc(sizeof(managerState))) == NULL )
	{
		perror("manager::CreateState");
		exit(1);
	}

	memset(state, 0, sizeof(managerState));

	manager_setHost(state, serveraddress);
	manager_setPort(state, port);

	state->enableRemoteControl = remoteControl;

	if ( (state->stats = stats_createStats()) == NULL )
		return NULL;

	if ( (state->logger = logger_createLogger()) == NULL )
	{
		PRINTERROR("StartDownload::Unable to initialize logger");
		stats_freeStats(state->stats);
		return NULL;
	}

	if ( remoteControl )
	{
		if ( (state->remote = remotectrl_create(remoteControlPort, state->stats)) == NULL )
		{
			PRINTERROR("StartDownload::Unable to initialize remote control");
			stats_freeStats(state->stats);
			logger_freeLogger(state->logger);
			return NULL;
		}

		remotectrl_setCommandCallback(state->remote, manager_remoteControlCommand);
		remotectrl_setUserData(state->remote, state);

		remotectrl_setVersion(state->remote, MANAGER_VERSION);

		remotectrl_start(state->remote);
	}

	logger_addListener(state->logger, manager_logListener, state);

	return state;
}

void manager_remoteControlCommand(void* userData, char* key, char* value)
{
	managerState* state = userData;

	if ( strcmp(key, "paused") == 0 )
	{
		state->isPaused = strcmp(value, "1") == 0;
		stats_setIsPaused(state->stats, state->isPaused);

		if ( state->isPaused )
			logger_log(state->logger, "RemoteControl::Queue will be paused", NULL, NULL, NULL);
		else
			logger_log(state->logger, "RemoteControl::Queue will be unpaused", NULL, NULL, NULL);
	}
	else if ( strcmp(key, "exit") == 0 || strcmp(key, "quit") == 0 )
	{
		logger_log(state->logger, "RemoteControl::Exiting application", NULL, NULL, NULL);
		/* not to nice to quit like this, but fuck it. */
		exit(1);
	}
}

void manager_setNzbPath(void* state, const char* nzbpath)
{
	managerState* managerstate = state;

	if ( nzbpath != NULL && strlen(nzbpath) < sizeof(managerstate->nzbPath) )
		strcpy(managerstate->nzbPath, nzbpath);

}
void manager_setDownloadPath(void* state, const char* downloadpath)
{
	managerState* managerstate = state;

	if ( downloadpath != NULL && strlen(downloadpath) < sizeof(managerstate->downloadPath) )
		strcpy(managerstate->downloadPath, downloadpath);
}

void manager_setUsername(void* state, const char* username)
{
	managerState* managerstate = state;

	if ( username != NULL && strlen(username) < sizeof(managerstate->username) )
		strcpy(managerstate->username, username);
}

void manager_setPassword(void* state, const char* password)
{
	managerState* managerstate = state;

	if ( password != NULL && strlen(password) < sizeof(managerstate->password) )
		strcpy(managerstate->password, password);
}

void manager_setHost(void* state, const char* host)
{
	managerState* managerstate = state;

	if ( host != NULL && strlen(host) < sizeof(managerstate->host) )
		strcpy(managerstate->host, host);
}

void manager_setPort(void* state, int port)
{
	managerState* managerstate = state;

	managerstate->port = port;
}

void manager_setAuthentication(void* state, int authenticate)
{
	managerState* managerstate = state;

	managerstate->usesAuthentication = authenticate;
}

void manager_setFileCombine(void* state, int combine)
{
	managerState* managerstate = state;

	managerstate->enableFileCombine = combine;
}

void manager_setYEnc(void* state, int yenc)
{
	managerState* managerstate = state;

	managerstate->enableYEnc = yenc;
}

void* manager_getStats(void* state)
{
	managerState* managerstate = state;

	return managerstate->stats;
}

void manager_setMaxConnectionTries(void* state, int retries)
{
	managerState* managerstate = state;

	managerstate->maxConnectTries = retries;
}

void manager_setMaxArticleTries(void* state, int retries)
{
	managerState* managerstate = state;

	managerstate->maxArticleTries = retries;
}

void manager_setMaxThreads(void* state, int maxthreads)
{
	managerState* managerstate = state;

	managerstate->maxThreads = maxthreads;

	if ( (managerstate->threadPool = realloc(managerstate->threadPool, sizeof(pthread_t)*maxthreads)) == NULL )
	{
		perror("manager::setMaxThreads");
		exit(1);
	}
}

/**
	The method invoked in worker threads.
*/
void* manager_downloadThread(void* managerstate)
{
	int status = 0;
	managerState* state = managerstate;
	nzbFilePart* article = NULL;
	nzbFile* file = NULL;

	void* nntpState = NULL;
	int isconnected = 0;
	int continueFlag = 1;
	void* queueItem = NULL;
	int tries = 0;

	/* Rules
		*	When manager is paused, disconnect and sleep until unpaused
		*	When connecting fails, close the thread
		*	Take items from queue until queue is empty
		*	When an item fails because of a timeout, disconnect and reconnect
		*	When an item fails, retry until maximum tries is exhausted
		*	When a queue item is EOF, all the parts of a file are received.
		*	Depending on configuration, decode into target file and remove temp files
		*	Files are not removed on decode failure
		*	When decoding is disabled, files will not be deleted
		*	When file combining is enabled and decoding failed, files are combined
		*	When file combining is enabled and decoding is disabled, files are combined
		*	When the queue is eof and it failed, files are not deleted
		*	When the queue is eof and it failed and file combining is enabled, files are combined
		*	When file combining failed, files are not deleted
	*/

	while ( 1 )
	{
		while ( state->isPaused )
		{
			if ( isconnected )
			{
				nntp_close(nntpState);
				isconnected = 0;
			}
			
			SLEEP(10);
		}

		if ( !isconnected )
		{
			if ( (status = manager_connectNntp(&nntpState, state)) != NNTP_OK )
			{
				logger_log(state->logger, "DownloadThread::Could not connect!, exiting thread", NULL, NULL, NULL);
				
				if ( nntpState != NULL )
					nntp_close(nntpState);

				return 0;
			}
			isconnected =1;
		}

		tries = 0;

		if ( (queueItem = nzbqueue_getQueueItem(state->currentNzbQueue)) == NULL )
			break;

		file = nzbqueue_getQueueItemFile(state->currentNzbQueue, queueItem);

		article = nzbqueue_getQueueItemFilePart(state->currentNzbQueue, queueItem);

		while ( continueFlag )
		{
			switch ( nzbqueue_getQueueItemType(state->currentNzbQueue, queueItem) )
			{
				case QUEUE_ITEM_TYPE_PART:
				{
					switch ( (status = manager_downloadArticle(nntpState, file, article, state->downloadPath)) )
					{
						case NNTP_TIMEOUT:
						case NNTP_ERROR:
						{
							if ( status == NNTP_TIMEOUT )
							{
								nntp_close(nntpState);
								isconnected =0;

								logger_log(state->logger, "DownloadThread::Timeout, reconnecting", NULL, NULL, NULL);
								
								if ( (status = manager_connectNntp(&nntpState, state)) != NNTP_OK )
								{
									logger_log(state->logger, "DownloadThread::Could not reconnect!", NULL, NULL, NULL);
									continueFlag = 0;
								}
								else
									isconnected =1;
							}

							if ( ++tries == state->maxArticleTries )
							{
								logger_log(state->logger, "Error downloading article.\n", NULL, NULL, NULL);
								stats_addFailedCount(state->stats, 1);
								stats_addProcessed(state->stats, 1);
								continueFlag = 0;
								nzbqueue_setQueueItemStatus(state->currentNzbQueue, queueItem, QUEUE_ITEM_STATE_FAILED);
							}
							else
								logger_log(state->logger, "Error downloading article, retrying..%s", article->id, NULL, NULL);

							break;
						}
						case NNTP_ERROR_ARTICLE:
						case NNTP_ERROR_GROUP:
							continueFlag = 0;
							nzbqueue_setQueueItemStatus(state->currentNzbQueue, queueItem, QUEUE_ITEM_STATE_FAILED);
							logger_log(state->logger, "Article not found..%s", article->id, NULL, NULL);
							stats_addFailedCount(state->stats, 1);
							stats_addProcessed(state->stats, 1);
							break;
						default:
							continueFlag = 0;
							nzbqueue_setQueueItemStatus(state->currentNzbQueue, queueItem, QUEUE_ITEM_STATE_SUCCESS);
							stats_addProcessed(state->stats, 1);
							break;
					}
					break;
				}
				case QUEUE_ITEM_TYPE_EOF:
				{
					char full_path[1024];
					sprintf(full_path, "%s/%s", state->downloadPath, file->filename);

					switch ( nzbqueue_getQueueItemStatus(state->currentNzbQueue, queueItem) )
					{
						case QUEUE_ITEM_STATE_PROCESSING:
						{
							if ( state->enableYEnc )
							{
								if ( !ydec_decodeCollection(full_path, state->downloadPath, nzbqueue_getQueueItemPartNumber(state->currentNzbQueue, queueItem)) )
								{
									if ( state->enableFileCombine )
									{
										if ( manager_combineFiles(state, full_path, full_path, nzbqueue_getQueueItemPartNumber(state->currentNzbQueue, queueItem)) )
											manager_deleteFiles(full_path, nzbqueue_getQueueItemPartNumber(state->currentNzbQueue, queueItem));
									}

									logger_log(state->logger, "DownloadThread::Decoding of %s failed", file->filename, NULL, NULL);
								}
								else
								{
									logger_log(state->logger, "DownloadThread::Successfully processed %s", file->filename, NULL, NULL);
									
									manager_deleteFiles(full_path, nzbqueue_getQueueItemPartNumber(state->currentNzbQueue, queueItem));
								}
							}
							else
							{
								if ( state->enableFileCombine )
								{
									if ( manager_combineFiles(state, full_path, full_path, nzbqueue_getQueueItemPartNumber(state->currentNzbQueue, queueItem)) )
										manager_deleteFiles(full_path, nzbqueue_getQueueItemPartNumber(state->currentNzbQueue, queueItem));
								}

								logger_log(state->logger, "DownloadThread::Successfully processed %s", file->filename, NULL, NULL);
							}

							nzbqueue_setQueueItemStatus(state->currentNzbQueue, queueItem, QUEUE_ITEM_STATE_SUCCESS);

							break;
						}
						case QUEUE_ITEM_STATE_FAILED:
						{
							logger_log(state->logger, "DownloadThread::Failed downloading %s", file->filename, NULL, NULL);
							
							if ( state->enableFileCombine )
							{
								if ( manager_combineFiles(state, full_path, full_path, nzbqueue_getQueueItemPartNumber(state->currentNzbQueue, queueItem)) )
									manager_deleteFiles(full_path, nzbqueue_getQueueItemPartNumber(state->currentNzbQueue, queueItem));
							}
								
							nzbqueue_setQueueItemStatus(state->currentNzbQueue, queueItem, QUEUE_ITEM_STATE_FAILED);

							break;
						}
					}
					
					continueFlag = 0;

					break;
				}
			}
		}

		continueFlag = 1;
	}

	nntp_close(nntpState);
	
	return 0;
}

/** 
	Combine collection of files into one
*/
int manager_combineFiles(void*state, char* basePath, char* targetPath, int files)
{
	int i =0;
	char path[1024];
	FILE* fpout = NULL;
	managerState* manager = state;
	FILE* fpin = NULL;
	char buffer[2048];
	int read = 0;

	if ( (fpout = fopen(targetPath, "wb+")) == NULL )
	{
		logger_log(manager->logger, "Manager::CombineFiles: Could not create target", NULL, NULL, NULL);
		return 0;
	}

	for ( i=1;i<=files;i++ )
	{
		sprintf(path, "%s.%i", basePath, i);	
		
		if ( (fpin = fopen(path, "rb")) != NULL )
		{
			while ( (read = fread(buffer, 1, sizeof(buffer), fpin)) > 0 )
			{
				if ( fwrite(buffer, 1, sizeof(buffer), fpout) <= 0 )
				{
					logger_log(manager->logger, "Manager::CombineFiles: Error writing target", NULL, NULL, NULL);
					fclose(fpout);
					fclose(fpin);
					return 0;
				}
			}
			fclose(fpin);
		}
		else
			logger_log(manager->logger, "Manager::CombineFiles: Could not read part file", NULL, NULL, NULL);
	}

	fclose(fpout);
	return 1;
}

/** 
	Removes temporary files
*/
void manager_deleteFiles(char* basePath, int files)
{
	int i =0;
	char path[1024];

	for ( i=1;i<=files;i++ )
	{
		sprintf(path, "%s.%i", basePath, i);	

		filesys_deleteFile(path);
	}
}

void manager_remoteControlFileReceived(void* userData, char* filename, char* path, long size)
{
	managerState* state = userData;
	int len = strlen(path);

	if ( len > 3 )
	{
		if ( strcmp(path+len-4, ".nzb") == 0 )
		{
			char targetTmpPath[1024];
			char targetPath[1024];
			FILE* fpin = NULL;
			FILE* fpout = NULL;

			if ( (fpin = fopen(path, "rb")) != NULL )
			{
				strcpy(targetTmpPath, state->nzbPath);
				strcat(targetTmpPath, filename);
				strcat(targetTmpPath, ".bak"); 
				/*rename file, so it is not picked up by the main application 
				while we are still processing. */

				if ( (fpout = fopen(targetTmpPath, "wb+")) != NULL )
				{
					char buffer[2048];
					int bytesRead = 0;
					while ( (bytesRead = fread(buffer, sizeof(buffer), 1, fpin)) > 0 )
					{
						if ( fwrite(buffer, bytesRead, 1, fpout) <= 0 )
						{
							logger_log(state->logger, "Manager::RemoteControlFileReceived: Failed to copy file!", NULL, NULL, NULL);
							break;
						}
						size -= bytesRead;
					}
					fclose(fpout);
					fclose(fpin);
				}
			}

			strcpy(targetPath, state->nzbPath);
			strcat(targetPath, filename);

			filesys_moveFile(targetTmpPath, targetPath);
			
		}	
	}
}

/** 
	Downloads article
*/
int manager_downloadArticle(void* state, nzbFile* file, nzbFilePart* article, char* path)
{
	char full_path[1024];

	int status = 0;

	if ( nntp_selectGroup(state, file->group) != NNTP_OK )
		return NNTP_ERROR_GROUP;

	/* create temp file based on path, name and segment number */
	sprintf(full_path, "%s/%s.%i", path, file->filename, article->segment);

	if ( (status = nntp_downloadArticleToFile(state, article->id, full_path, manager_downloadArticleProgress)) != NNTP_OK )
	{

		//delete if download failed
		filesys_deleteFile(full_path);

		return status;
	}

	return NNTP_OK;
}

void manager_downloadArticleProgress(void* userdata, int downloadSize)
{
	managerState* state = userdata;

	stats_addBytesDownloaded(state->stats, downloadSize);
}

int manager_connectNntp(void** nntpstate, void* managerstate)
{
	int i = 0;
	
	managerState* state = managerstate;

	if ( state->host == NULL || (state->usesAuthentication && (state->username == NULL || state->password == NULL)) || state->maxConnectTries == 0
		|| state->port == 0 )
	{
		logger_log(state->logger, "connectNntp::Corrupt cfg file!", NULL, NULL, NULL);
		exit(1);
	}

	/* keep trying until max attempts have exhausted */
	for ( i=1; i<state->maxConnectTries; i++ )
	{
		if ( (*nntpstate = nntp_connect(state->host, state->port)) != NULL )
		{
			nntp_setUserData(*nntpstate, managerstate);

			switch ( nntp_login(*nntpstate, strcmp(state->username,"")==0? NULL:state->username, state->password) )
			 {
				case NNTP_OK:
					return NNTP_OK;
				case NNTP_ERROR_LOGIN:
					logger_log(state->logger, "connectNntp:Invalid credentials!", NULL, NULL, NULL);
					//return NNTP_ERROR;
			 }
		}

		logger_log(state->logger, "connectNntp:Error connecting, retrying", NULL, NULL, NULL);
		SLEEP(10);
	}

	return NNTP_ERROR;
}


void manager_freeState(void* state)
{
	managerState* managerstate = state;
	
	free(managerstate->threadPool);

	if ( managerstate->currentNzbQueue != NULL )
		nzbqueue_freeQueue(managerstate->currentNzbQueue);
	
	if ( managerstate->currentNzbState != NULL )
		nzb_closeNzb(managerstate->currentNzbState);

	stats_freeStats(managerstate->stats);
	logger_freeLogger(managerstate->logger);

	if ( managerstate->enableRemoteControl )
		remotectrl_free(managerstate->remote);

	free(managerstate);
}
